﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.TeamFoundation.WorkItemTracking.Client;

namespace TfsRollUp.Core
{
    /// <summary>
    /// WorkItem helpers
    /// </summary>
    public static class WorkItemExtensions
    {
        private static void InternalCalculateTree(ReadOnlyCollection<WorkItem> workItemTree, WorkItemLinkInfo[] children, string fieldName, Settings settings)
        {
            if (workItemTree == null || workItemTree.Count == 0)
            {
                Trace.TraceWarning("{0}.InternalCalculateTree: tree is empty", Constants.TfsRollUpPlugin);
                return;
            }
            if (children == null || children.Length == 0)
            {
                Trace.TraceWarning("{0}.InternalCalculateTree: children is empty", Constants.TfsRollUpPlugin);
                return;
            }
            if (string.IsNullOrWhiteSpace(fieldName))
            {
                Trace.TraceWarning("{0}.InternalCalculateTree: fieldName is empty", Constants.TfsRollUpPlugin);
                return;
            }

            WorkItemNode calculationRoot = new WorkItemNode(null, workItemTree[0].Id, 0.0);
            for (int i = 1; i < children.Count(); i++)
            {
                var parent = calculationRoot.AllNodes[children[i].SourceId];
                if (parent == null)
                {
                    throw new ArgumentNullException(string.Format("{0}.CalculateTree:No parent found with id:{1}", Constants.TfsRollUpPlugin, children[i].SourceId));
                }
                var childWorkItem = (from p in workItemTree where p.Id == children[i].TargetId select p).SingleOrDefault();
                if (childWorkItem == null)
                {
                    Trace.TraceError("{0}.InternalCalculateTree: Workitem id:{1} not found in tree", Constants.TfsRollUpPlugin, children[i].TargetId);
                    continue;
                }
                if (childWorkItem.Fields.Contains(fieldName))
                {
                    var node = new WorkItemNode(parent, children[i].TargetId, (double)childWorkItem.Fields[fieldName].Value);
                }
                else
                {
                    Trace.TraceInformation("{0}.InternalCalculateTree: Workitem id:{1} does not contain the field:'{2}'", Constants.TfsRollUpPlugin, childWorkItem.Id, fieldName);
                    var node = new WorkItemNode(parent, children[i].TargetId, 0.0);
                }
            }

            calculationRoot.CalculateChildren();
            if (settings.IsWorkItemOnMonitorList(calculationRoot.Id))
            {
                GenerateCalculationInformationResult(workItemTree[0], children, fieldName, calculationRoot, settings);
            }

            // Transfer the results back from the WorkItemNode tree to the workitems
            var rollUpFieldName = Constants.FieldNameTfsRollUpPrefix + fieldName;
            foreach (var currentWorkItem in workItemTree)
            {
                if (currentWorkItem.Fields.Contains(rollUpFieldName))
                {
                    var rollUpOriginalValue = (double)(currentWorkItem.Fields[rollUpFieldName].Value ?? 0.0);
                    var node = calculationRoot.AllNodes[currentWorkItem.Id];
                    if (Math.Abs(rollUpOriginalValue - node.TotalUnitOfTimeChildren) > 0.01)
                    {
                        Trace.TraceInformation("{0}.InternalCalculateTree: Field:{1} updated with value:{2} for WorkItem:{3}", Constants.TfsRollUpPlugin, rollUpFieldName, node.TotalUnitOfTimeChildren, currentWorkItem.Id);
                        currentWorkItem.Fields[rollUpFieldName].Value = node.TotalUnitOfTimeChildren;
                    }
                }
            }
        }

        private static void GenerateCalculationInformationResult(WorkItem workItemRoot, WorkItemLinkInfo[] children, string fieldName, WorkItemNode calculationRoot, Settings settings)
        {
            const int MaxDgmlAttachments = 10;
            var fileName = Path.Combine(Environment.GetEnvironmentVariable("TEMP"), fieldName + workItemRoot.Id.ToString() + DateTime.UtcNow.Ticks.ToString() + ".dgml");
            //  http://localhost:8080/tfs/web/UI/Pages/WorkItems/WorkItemEdit.aspx?id={0}
            var dgmlText = calculationRoot.CalculationResult(children, settings.TswaLink);
            File.WriteAllText(fileName, dgmlText);

            // Make sure sure that DGML attachments never grows beyond MaxDgmlAttachments
            var dgmlAttachments = new List<Attachment>();
            foreach (Attachment p in workItemRoot.Attachments)
            {
                if (p.Extension.ToUpperInvariant() == ".DGML")
                {
                    dgmlAttachments.Add(p);
                }
            }
            if (dgmlAttachments.Count > MaxDgmlAttachments)
            {
                for (int i = 0; i < dgmlAttachments.Count - MaxDgmlAttachments; i++)
                {
                    workItemRoot.Attachments.Remove(dgmlAttachments[dgmlAttachments.Count - 1 - i]);
                }
            }
            workItemRoot.Attachments.Add(new Attachment(fileName));
        }

        private static string GetSelectWiql(string[] fieldList)
        {
            return string.Format("SELECT System.ID,{0} FROM WorkItems", String.Join(",", fieldList));
        }

        /// <summary>
        /// Get a unique key for this workitem based on TfsCollection, TeamProject and WorkItemId
        /// </summary>
        /// <param name="workItem"></param>
        /// <returns></returns>
        public static string CollectionTeamProjectIdKey(this WorkItem workItem)
        {
            return string.Format("{0}.{1}.{2}", workItem.Store.TeamProjectCollection.Uri.AbsolutePath,
                workItem.Fields[CoreField.TeamProject].Value.ToString(),
                workItem.Id.ToString());
        }

        /// <summary>
        /// Climb the tree to find the root, first one in the returned array is the parent
        /// </summary>
        /// <param name="workItem">Find root for this workitem</param>
        /// <param name="fieldList">Fields that should be included in the wiql</param>
        /// <returns>Array of the workitems read, first one is the parent</returns>
        public static ReadOnlyCollection<WorkItem> Root(this WorkItem workItem, string[] fieldList)
        {
            List<WorkItem> workItemsRead = new List<WorkItem>();
            if (workItem == null)
            {
                return null;
            }
            workItemsRead.Add(workItem);
            if (workItem.RelatedLinkCount == 0)
            {
                return workItemsRead.AsReadOnly();
            }
            if (workItem.WorkItemLinks == null || workItem.WorkItemLinks.Count == 0)
            {
                return workItemsRead.AsReadOnly();
            }
            var currentWorkItem = workItem;
            var wiql = GetSelectWiql(fieldList);
            var keepClimbing = true;
            while (keepClimbing)
            {
                keepClimbing = false;
                foreach (WorkItemLink link in currentWorkItem.WorkItemLinks)
                {
                    if (link.LinkTypeEnd.LinkType.IsOneToMany &&
                        link.LinkTypeEnd.LinkType.LinkTopology == WorkItemLinkType.Topology.Tree &&
                        link.LinkTypeEnd.Name == "Parent")
                    {
                        var queryResult = workItem.Store.Query(new int[] { link.TargetId }, wiql);
                        if (queryResult != null && queryResult.Count == 1)
                        {
                            currentWorkItem = queryResult[0];
                            workItemsRead.Add(currentWorkItem);
                            keepClimbing = true;
                        }
                        break;
                    }
                }
            }
            // Make root the first element
            workItemsRead.Reverse();
            return workItemsRead.AsReadOnly();
        }

        /// <summary>
        /// Get all children workitemlinks for this workitem
        /// http://blogs.msdn.com/b/allclark/archive/2010/05/06/sample-code-refactoring-to-handle-different-types-of-work-items.aspx
        /// </summary>
        /// <param name="workItem"></param>
        /// <returns>Array of <see cref="WorkItemLinkInfo"></see></returns>
        public static WorkItemLinkInfo[] Children(this WorkItem workItem)
        {
            if (workItem == null) throw new ArgumentNullException("workItem");
            var treeQueryText = string.Format("SELECT [System.Id] FROM WorkItemLinks WHERE ([Source].[System.Id] = {0}) AND [System.Links.LinkType] = 'System.LinkTypes.Hierarchy-Forward' mode(Recursive)", workItem.Id);
            var treeQuery = new Query(workItem.Store, treeQueryText);
            return treeQuery.RunLinkQuery();
        }

        /// <summary>
        /// Get the full unsorted tree for this workItem
        /// </summary>
        /// <param name="workItem"></param>
        /// <param name="fieldList">List of ReferenceName fields to read</param>
        /// <returns></returns>
        public static ReadOnlyCollection<WorkItem> Tree(this WorkItem workItem, string[] fieldList)
        {
            if (workItem == null) throw new ArgumentNullException("workItem");
            if (fieldList == null) throw new ArgumentNullException("fieldList");

            var root = workItem.Root(fieldList);
            if (root == null || root.Count == 0)
            {
                return null;
            }
            var tree = new List<WorkItem>(root);
            var childLinkInfoList = root[0].Children();

            // Get the list of workitemids that we need to read
            // Join SourceId & TargetId where id > 0
            var ids = (from p in childLinkInfoList where p.SourceId > 0 select p.SourceId).Union(from p in childLinkInfoList where p.TargetId > 0 select p.TargetId);
            if (ids == null || ids.Count() == 0)
            {
                return tree.AsReadOnly();
            }
            var workItemIds = ids.Except(root.Select(p => p.Id)).ToArray();
            var workItemList = workItem.Store.Query(workItemIds, GetSelectWiql(fieldList));
            if (workItemList == null || workItemList.Count == 0)
            {
                return tree.AsReadOnly();
            }
            foreach (WorkItem child in workItemList)
            {
                tree.Add(child);
            }
            return tree.AsReadOnly();
        }

        /// <summary>
        /// Save the tree
        /// </summary>
        /// <param name="workItem"></param>
        /// <param name="tree"></param>
        /// <returns></returns>
        public static BatchSaveError[] SaveTree(this WorkItem workItem, ReadOnlyCollection<WorkItem> tree)
        {
            var dirtyWorkItems = (from p in tree where p.IsDirty == true select p).ToArray();
            if (dirtyWorkItems == null || dirtyWorkItems.Length == 0)
            {
                Trace.TraceInformation("{0}.SaveTree: No changes to save", Constants.TfsRollUpPlugin);
                return null;
            }
            Trace.TraceInformation("{0}.SaveTree: {1} dirty workitems found", Constants.TfsRollUpPlugin, dirtyWorkItems.Length);
            BatchSaveError[] saveErrors = workItem.Store.BatchSave(dirtyWorkItems);
            return saveErrors;
        }

        /// <summary>
        /// The root for workItem is found and totals calculated for all children in the full tree.
        /// </summary>
        /// <param name="workItem">workItem to get root for</param>
        /// <param name="fieldList">list of fields that should be totaled</param>
        /// <param name="settings">Global settings</param>
        /// <returns>A collection of potentially recalculated workitems</returns>
        public static ReadOnlyCollection<WorkItem> CalculateTree(this WorkItem workItem, string[] fieldList, Settings settings)
        {
            var tree = workItem.Tree(fieldList);
            var rollUpFields = from p in fieldList where p.StartsWith(Constants.FieldNameTfsRollUpPrefix) select p.Substring(Constants.FieldNameTfsRollUpPrefix.Length);
            var root = tree[0];
            var children = root.Children();
            foreach (var fieldName in rollUpFields)
            {
                InternalCalculateTree(tree, children, fieldName, settings);
            }
            return tree;
        }
    }
}
